As a programmer, you are certainly familiar with the process of accessing individual items contained within a simple array using the index operator ([]), for example:
static void Main(string[] args) { // Loop over incoming command line arguments // using index operator. for(int i = 0; i < args.Length; i++) Console.WriteLine("Args: {0}", args[i]); // Declare an array of local integers. int[] myInts = { 10, 9, 100, 432, 9874}; // Use the index operator to access each element. for(int j = 0; j < myInts.Length; j++) Console.WriteLine("Index {0} = {1} ", j, myInts[j]); Console.ReadLine(); }
This code is by no means a major newsflash. However, the C# language provides the capability to design custom classes and structures that may be indexed just like a standard array, by defining an indexer method. This particular feature is most useful when you are creating custom collection classes (generic or nongeneric).
Before examining how to implement a custom indexer, let’s begin by seeing one in action. Assume you have added support for an indexer method to the custom PeopleCollection type developed in Chapter 10 (specifically, the CustomNonGenericCollection project). Observe the following usage within a new Console Application named SimpleIndexer:
// Indexers allow you to access items in an array-like fashion. class Program { static void Main(string[] args) { Console.WriteLine("***** Fun with Indexers *****\n"); PeopleCollection myPeople = new PeopleCollection(); // Add objects with indexer syntax. myPeople[0] = new Person("Homer", "Simpson", 40); myPeople[1] = new Person("Marge", "Simpson", 38); myPeople[2] = new Person("Lisa", "Simpson", 9); myPeople[3] = new Person("Bart", "Simpson", 7); myPeople[4] = new Person("Maggie", "Simpson", 2); // Now obtain and display each item using indexer. for (int i = 0; i < myPeople.Count; i++) { Console.WriteLine("Person number: {0}", i); Console.WriteLine("Name: {0} {1}", myPeople[i].FirstName, myPeople[i].LastName); Console.WriteLine("Age: {0}", myPeople[i].Age); Console.WriteLine(); } } }
As you can see, indexers behave much like a custom collection supporting the IEnumerator and IEnumerable interfaces (or their generic counterparts) in that they provide access to a container’s subitems. The major difference, of course, is that rather than accessing the contents using the foreach construct, you are able to manipulate the internal collection of sub-objects just like a standard array.
Now for the big question: How do you configure the PeopleCollection class (or any custom class or structure) to support this functionality? An indexer is represented as a slightly modified C# property definition. In its simplest form, an indexer is created using the this[] syntax. Here is the required update for the PeopleCollection class from Chapter 10:
// Add the indexer to the existing class definition. public class PeopleCollection : IEnumerable { private ArrayList arPeople = new ArrayList(); // Custom indexer for this class. public Person this[int index] { get { return (Person)arPeople[index]; } set { arPeople.Insert(index, value); } } ... }
Apart from using the this keyword, the indexer looks just like any other C# property declaration. For example, the role of the get scope is to return the correct object to the caller. Here, we are doing so by delegating the request to the indexer of the ArrayList object! The set scope is in charge this example, this is achieved by calling the Insert() method of the ArrayList.
As you can see, indexers are yet another form of syntactic sugar, given that this functionality can also be achieved using “normal” public methods such as AddPerson() or GetPerson(). Nevertheless, when you support indexer methods on your custom collection types, they integrate well into the fabric of the .NET base class libraries.
While creating indexer methods is quite commonplace when you are building custom collections, do remember that generic types give you this very functionality out of the box. Consider the following method, which makes use of a generic List<T> of Person objects. Note that you can simply use the indexer of List<T> directly, for example:
static void UseGenericListOfPeople() { List<Person> myPeople = new List<Person>(); myPeople.Add(new Person("Lisa", "Simpson", 9)); myPeople.Add(new Person("Bart", "Simpson", 7)); // Change first person with indexer. myPeople[0] = new Person("Maggie", "Simpson", 2); // Now obtain and display each item using indexer. for (int i = 0; i < myPeople.Count; i++) { Console.WriteLine("Person number: {0}", i); Console.WriteLine("Name: {0} {1}", myPeople[i].FirstName, myPeople[i].LastName); Console.WriteLine("Age: {0}", myPeople[i].Age); Console.WriteLine(); } }
Source Code The SimpleIndexer project is located under the Chapter 12 subdirectory.
The current PeopleCollection class defined an indexer that allowed the caller to identify subitems using a numerical value. Understand, however, that this is not a requirement of an indexer method. Suppose you’d prefer to contain the Person objects using a System.Collections.Generic.Dictionary<TKey, TValue> rather than an ArrayList. Given that ListDictionary types allow access to the contained types using a string token (such as a person’s first name), you could define an indexer as follows:
public class PeopleCollection : IEnumerable { private Dictionary<string, Person> listPeople = new Dictionary<string, Person>(); // This indexer returns a person based on a string index. public Person this[string name] { get { return (Person)listPeople[name]; } set { listPeople[name] = value; } } public void ClearPeople() { listPeople.Clear(); } public int Count { get { return listPeople.Count; } } IEnumerator IEnumerable.GetEnumerator() { return listPeople.GetEnumerator(); } }
The caller would now be able to interact with the contained Person objects as shown here:
static void Main(string[] args) { Console.WriteLine("***** Fun with Indexers *****\n"); PeopleCollection myPeople = new PeopleCollection(); myPeople["Homer"] = new Person("Homer", "Simpson", 40); myPeople["Marge"] = new Person("Marge", "Simpson", 38); // Get "Homer" and print data. Person homer = myPeople["Homer"]; Console.WriteLine(homer.ToString()); Console.ReadLine(); }
Again, if you were to use the generic Dictionary<TKey, TValue> type directly, you’d gain the indexer method functionality out of the box, without building a custom, non-generic class supporting a string indexer.
Source Code The StringIndexer project is located under the Chapter 12 subdirectory.
Understand that indexer methods may be overloaded on a single class or structure. Thus, if it makes sense to allow the caller to access subitems using a numerical index or a string value, you might define multiple indexers for a single type. By way of example, if you have ever programmed with ADO.NET (.NET’s native database-access API), you may recall that the DataSet type supports a property named Tables, which returns to you a strongly typed DataTableCollection type. As it turns out, DataTableCollection defines three indexers to get and set DataTable objects—one by ordinal position, and the others by a friendly string moniker and optional containing namespace:
public sealed class DataTableCollection : InternalDataCollectionBase { ... // Overloaded indexers! public DataTable this[string name] { get; } public DataTable this[string name, string tableNamespace] { get; } public DataTable this[int index] { get; } }
Note that a number of types in the base class libraries support indexer methods. So be aware, even if your current project does not require you to build custom indexers for your classes and structures, that many types already support this syntax.
You can also create an indexer method that takes multiple parameters. Assume you have a custom collection that stores subitems in a 2D array. If this is the case, you may define an indexer method as follows:
public class SomeContainer { private int[,] my2DintArray = new int[10, 10]; public int this[int row, int column] { /* get or set value from 2D array */ } }
Again, unless you are building a highly stylized custom collection class, you won’t have much need to build a multi-dimensional indexer. Still, once again ADO.NET showcases how useful this construct can be. The ADO.NET DataTable is essentially a collection of rows and columns, much like a piece of graph paper or the general structure of a Microsoft Excel spreadsheet.
While DataTable objects are typically populated on your behalf using a related “data adapter,” the following code illustrates how to manually create an in-memory DataTable containing three columns (for the first name, last name, and age of each record). Notice how once we have added a single row to the DataTable, we use a multidimensional indexer to drill into each column of the first (and only) row. (If you are following along, you’ll need to import the System.Data namespace into your code file.)
static void MultiIndexerWithDataTable() { // Make a simple DataTable with 3 columns. DataTable myTable = new DataTable(); myTable.Columns.Add(new DataColumn("FirstName")); myTable.Columns.Add(new DataColumn("LastName")); myTable.Columns.Add(new DataColumn("Age")); // Now add a row to the table. myTable.Rows.Add("Mel", "Appleby", 60); // Use multi-dimension indexer to get details of first row. Console.WriteLine("First Name: {0}", myTable.Rows[0][0]); Console.WriteLine("Last Name: {0}", myTable.Rows[0][1]); Console.WriteLine("Age : {0}", myTable.Rows[0][2]); }
Do be aware that we’ll take a rather deep dive into ADO.NET beginning with Chapter 21, so if some of the previous code seems unfamiliar, fear not. The main point of this example is that indexer methods can support multiple dimensions, and if used correctly, can simplify the way you interact with contained subobjects in custom collections.
Indexers can be defined on a given .NET interface type to allow supporting types to provide a custom implementation. Here is a simple example of an interface that defines a protocol for obtaining string objects using a numerical indexer:
public interface IStringContainer { // This interface defines an indexer that returns // strings based on a numerical index. string this[int index] { get; set; } }
With this interface definition, any class or structure that implements this interface must now support a read/write indexer that manipulates subitems using a numerical value.
That wraps up the first major topic of this chapter. While understanding the syntax of C# indexers is important, as explained in Chapter 10, typically the only time a programmer needs to build a custom generic collection class is to add constraints to the type parameters. If you happen to build such a class, adding a custom indexer will make your collection class look and feel like the standard collection classes in the .NET base class libraries.
Now let’s examine a language feature that lets you build custom classes or structures that respond uniquely to the intrinsic operators of C#. Allow me to introduce operator overloading.